package erfgame.frontend.desktop.view.terrain.iso;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import erfgame.core.Dimensions3D;
import erfgame.core.Point3D;
import erfgame.core.Point3DAndRotation;
import erfgame.core.Rectangle3D;
import erfgame.core.Rotation;
import erfgame.core.dao.ObjectDAO;
import erfgame.core.entity.Entity;
import erfgame.core.entity.EntityChangeEvent;
import erfgame.core.entity.EntityEvent;
import erfgame.core.entity.EntityEventType;
import erfgame.core.entity.EntityListener;
import erfgame.core.entity.Physical;
import erfgame.core.event.Event;
import erfgame.core.event.queue.memory.InMemoryEventQueue;
import erfgame.core.graph.simple.Node;
import erfgame.core.graphics.renderer.EntityRenderer;
import erfgame.core.graphics.renderer.RendererContext;
import erfgame.core.graphics.renderer.chain.EntityRendererChainFactory;
import erfgame.core.graphics.renderer.chain.RendererChain;
import erfgame.core.memory.Pointer;
import erfgame.core.path.PathElement;
import erfgame.core.util.ColorUtils;
import erfgame.core.world.DefaultWorldContext;
import erfgame.core.world.World;
import erfgame.core.world.WorldContext;
import erfgame.core.world.WorldDAOManager;
import erfgame.core.world.WorldEventQueueRunner;
import erfgame.core.world.terrain.BlockGridTiles;
import erfgame.core.world.terrain.BlockSurfaceImageFactory;
import erfgame.core.world.terrain.BlockSurfaceImages;
import erfgame.core.world.terrain.GridTile;
import erfgame.core.world.terrain.PixelDepth;
import erfgame.core.world.terrain.TerrainConstants;
import erfgame.core.world.terrain.TerrainUtils;
import erfgame.core.world.terrain.WalkablePath;
import erfgame.core.world.terrain.insertion.SurfaceInsertion;
import erfgame.core.world.terrain.insertion.SurfaceRendererInsertion;
import erfgame.core.world.terrain.rotated.RotatedWorld;
import erfgame.core.world.terrain.rotated.TransformedBlock;

import erfgame.frontend.desktop.view.terrain.iso.handler.InteractionHandler;

@SuppressWarnings(value="serial")
public class IsometricTerrainView 
	//extends Canvas 
	extends JComponent
	implements ObjectDAO<Point3DAndRotation, BlockSurfaceImages>, ViewportListener, RendererContext, WorldContext, EntityListener, RotationContext {
	
	private static final Logger log = Logger.getLogger( IsometricTerrainView.class.getName() );
	
	private ObjectDAO<Point3DAndRotation, BlockSurfaceImages> surfaceImagesDAO;
	private ObjectDAO<Point, BlockGridTiles> surfacesDAO;
	private ObjectDAO<Point, int[][][]> surfaceDAO;
	private ObjectDAO<Point3D, Pointer<byte[][][]>> terrainDAO;
	
	/*package*/ WorldDAOManager worldDAOManager;
	private DefaultWorldContext worldContext;
	
	private RotatedWorld rotatedWorld;
	
	private int blockBuffer;
	
	private ModdingBlockSurfaceImageViewport viewport;
	
	private BlockGridTiles[][] blockGridTiles;
	
	private int highlightedGridX = 1;
	private int highlightedGridY = 1;
	private int highlightedIndex = -1;
	private boolean renderAll;
	private List<PathElement> highlightedPath;
	
	private boolean showSurfaces;
	private boolean showPaths;
	private boolean showGridTiles;
	private boolean showDepths;
	private boolean showSurfaceCounts;
	private boolean showPositions;
	private boolean showEvents;
	
	private IsometricTerrainViewEventAdapter eventAdapter;
	
	private EntityRendererChainFactory rendererFactory;
	
	private Map<Entity, RendererChain> knownEntities = new HashMap<Entity, RendererChain>();
	
	private ActionListener closeActionListener;
	
	private BufferedImage bufferImage;
	
	private WorldEventQueueRunner eventQueueRunner;
	
	public IsometricTerrainView( 
			WorldDAOManager worldDAOManager,
			EntityRendererChainFactory rendererFactory,
			int blockBuffer 
	) {
		this.worldDAOManager = worldDAOManager;
		this.blockBuffer = blockBuffer;
		this.rendererFactory = rendererFactory;
		
		this.setEnabled( true ) ;
		this.setFocusable( true );
		
		this.eventAdapter = new IsometricTerrainViewEventAdapter();
		this.addMouseListener( this.eventAdapter );
		this.addMouseMotionListener( this.eventAdapter );
		this.addMouseWheelListener( this.eventAdapter );
		this.addKeyListener( this.eventAdapter );
		
		this.worldContext = new DefaultWorldContext();
		
		this.addComponentListener( new ComponentAdapter() {

			@Override
			public void componentResized(ComponentEvent e) {
				// TODO : thread this!
				bufferImage = null;
				requestFocus();
				rebuildBlocks();
			}
		});
		
		this.addKeyListener( new KeyAdapter() {
			@Override
			public void keyTyped(KeyEvent e) {
				World world = getCurrentWorld();
				switch( e.getKeyChar() ) { 
				case 'a':
					showPaths = !showPaths;
					repaint();
					break;
				case 'e':
					showEvents = !showEvents;
					repaint();
					break;
				case 'g':
					showGridTiles = !showGridTiles;
					repaint();
					break;
				case 'r':
					rebuildBlocks();
					break;
				case 'i':
					renderAll = !renderAll;
					repaint();
					break;
				case 's':
					showSurfaces = !showSurfaces;
					repaint();
					break;
				case 'o':
					showPositions = !showPositions;
					repaint();
					break;
				case 'b':
					TerrainUtils.regenerateSurfaces(
							0, 
							0, 
							world.getBlocksX(), 
							world.getBlocksY(), 
							surfaceDAO, 
							terrainDAO, 
							(byte[][][])null, 
							world
					);
					TerrainUtils.regenerateGridTiles(
							0, 
							0, 
							world.getBlocksX(), 
							world.getBlocksY(), 
							surfaceDAO, 
							IsometricTerrainView.this.surfacesDAO, 
							world
					);
					repaint();
					break;
				case 'd':
					showDepths = !showDepths;
					repaint();
					break;
				case 'c':
					showSurfaceCounts = !showSurfaceCounts;
					repaint();
					break;
				case 'x':
					destroy();
					if( closeActionListener != null ) {
						closeActionListener.actionPerformed( new ActionEvent( this, 0, null ) );
					}
					break;
				case 'p':
					if( eventQueueRunner != null ) {
						stop();
					} else {
						start();
					}
					break;
				}
			}
		});
		
	}
	
	public void stop() {
		if( this.eventQueueRunner != null ) {
			this.eventQueueRunner.stop();
			this.eventQueueRunner = null;
		}
	}
	
	public void start() {
		if( this.eventQueueRunner == null ) {
			WorldEventQueueRunner eventQueueRunner = new WorldEventQueueRunner(this.getCurrentWorld());
			Thread thread = new Thread( eventQueueRunner );
			thread.start();
			this.eventQueueRunner = eventQueueRunner;
		} else {
			throw new RuntimeException("already started");
		}
	}
	
	public void destroy() {
		stop();
		forgetAllEntities();
	}
	
	public void setCloseActionListener( ActionListener closeActionListener ) {
		this.closeActionListener = closeActionListener;
	}
	
	public void setInteractionHandlers( Collection<InteractionHandler> handlers ) {
		this.eventAdapter.interactionHandlers.clear();
		for( InteractionHandler interaction : handlers ) {
			this.addInteractionHandler( interaction );
		}
	}
	
	public ObjectDAO<Point3DAndRotation, BlockSurfaceImages> getSurfaceImagesDAO() {
		return this.surfaceImagesDAO;
		
	}
	
	public void highlightPath( List<PathElement> path ) {
		this.highlightedPath = path;
		repaint();
	}
	
	public void highlightGrid( int gx, int gy, int index ) {
		this.highlightedGridX = gx;
		this.highlightedGridY = gy;
		this.highlightedIndex = index;
		repaint();
	}
	
	public void adjustViewZ( int delta ) {
		viewport.setCurrentLayer( Math.max( 0, Math.min( viewport.getCurrentLayer() + delta, getCurrentWorld().getBlocksZ()-1 ) ) );
	}
	
	public int getViewZ() {
		return this.viewport.getCurrentLayer() * worldContext.getCurrentWorld().getBlockDepth();
	}
	
	public Rotation getCurrentRotation() {
		return viewport.getCurrentRotation();
	}
	
	public void setCurrentRotation( Rotation rotation, Point around ) {
		rebuildBlocks(rotation, around);
	}
	
	public void setCurrentWorld( World world ) {
		this.worldContext.setCurrentWorld(world);
		
		this.surfaceImagesDAO = worldDAOManager.getBlockSurfaceImagesDAO(world);
		this.surfacesDAO = worldDAOManager.getGridTilesDAO(world);
		this.surfaceDAO = worldDAOManager.getSurfacesDAO(world);
		this.terrainDAO = worldDAOManager.getBlockTerrainDAO(world);
		forgetAllEntities();
	}
	
	public World getCurrentWorld() {
		return this.worldContext.getCurrentWorld();
	}
	
	public void setWorldContext( DefaultWorldContext worldContext ) {
		this.worldContext = worldContext;
	}
	
	public int getWorldX() {
		return this.viewport.getWorldX();
	}
	
	public int getWorldY() {
		return this.viewport.getWorldY();
	}
	
	public void setWorldOffsets( int x, int y ) {
		this.viewport.setWorldOffsets( x, y );
	}
	  
//	public int getBlockX() {
//		return this.worldX / this.world.getBlockWidth();
//	}
//	
//	public int getBlockY() {
//		return this.worldY / this.world.getBlockHeight();
//	}
//	
	public int getBlockZ() {
		return this.viewport.getCurrentLayer();
	}
	
	public Component getComponent() {
		return this;
	}
	
	public void blockLoaded(int tx, int ty, int tz) {
		// TODO : only repaint the area that has changed
		repaint();
	}
	
	public void rebuildBlocks() {
		Rotation rotation;
		Point around;
		if( this.viewport != null ) {
			rotation = this.viewport.getCurrentRotation();
			around = new Point( 
					this.getWorldX() + this.getWidth()/2,
					this.getWorldY() + this.getHeight()
			);
		} else {
			rotation = Rotation.degrees0;
			around = new Point( this.getWidth()/2, this.getHeight() );
		}
		rebuildBlocks( rotation, around );
	}

	public void rebuildBlocks( Rotation rotation, Point around ) {

		stop();
		
		// forget all the known entities and remove listeners
		forgetAllEntities();
		
		World currentWorld = getCurrentWorld();
		RotatedWorld world = new RotatedWorld( currentWorld, rotation );
		this.rotatedWorld = world;
		
		int worldX;
		int worldY;
		int level;
		if( this.viewport != null ) {
			worldX = Math.max( 0, this.viewport.getWorldX() );
			worldY = Math.max( 0, this.viewport.getWorldY() );
			Point aroundTimes2 = new Point( world.getWorldWidth(), world.getWorldHeight() );
			Point rotated = rotation.rotate(
					worldX + this.getWidth()/2, 
					worldY + this.getHeight(), 
					aroundTimes2
			);
			worldX = Math.max( 0, Math.min( world.getWorldWidth() - this.getWidth(), rotated.x - this.getWidth()/2 ) );
			// TODO check maximum possible y
			worldY = Math.max( 0, rotated.y - this.getHeight() );
			level = viewport.getCurrentLayer();
		} else {
			worldX = 0;
			worldY = 0;
			level = 0;
		}
		
		int visibleBlocksWidth = this.getWidth() / world.getBlockWidth() + 1 + this.blockBuffer * 2;
		int visibleBlocksHeight = ((this.getHeight()+world.getWorldDepth())*2) / world.getBlockHeight() + 1 + this.blockBuffer * 2;
		
		int visibleBlocksDepth = world.getBlocksZ();
		BlockSurfaceImages[][][] visibleBlocks = new BlockSurfaceImages[ visibleBlocksWidth ][ visibleBlocksHeight ][ visibleBlocksDepth ];
		
		// TODO : this should be done in the AWT thread
		this.viewport = new ModdingBlockSurfaceImageViewport(
				visibleBlocks,
				visibleBlocksWidth,
				visibleBlocksHeight,
				world,
				this.surfaceImagesDAO, 
				worldDAOManager.getBlockEntityPositionsDAO(currentWorld),
				this.blockBuffer, 
				rotation,
				this.getSize(), 
				this, 
				this.rendererFactory
		);
		this.viewport.setViewportListener( this );
		this.viewport.setCurrentLayer(level);
		this.viewport.setWorldOffsets( worldX, worldY );
		//this.viewport.reload();
		int blocksX = currentWorld.getBlocksX();
		int blocksY = currentWorld.getBlocksY();
		this.blockGridTiles = new BlockGridTiles[blocksX][blocksY];
		for( int x=0; x<blocksX; x++ ) {
			for( int y=0; y<blocksY; y++ ) {
				Point b = new Point( x, y );
				this.blockGridTiles[x][y] = this.surfacesDAO.retrieveObject(b);
			}
		}
		
		start();
	}
	
	@Override
	public void update(Graphics g) {
		int width = getWidth();
		int height = getHeight();
		if( bufferImage == null ) {
			bufferImage = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB );
		}
		Graphics bufferGraphics = bufferImage.getGraphics();
		bufferGraphics.setClip(0, 0, width, height);
		render( bufferGraphics );
		bufferGraphics.dispose();
		paint( g );
	}

	@Override
	public void paint( Graphics g ) {
		if( bufferImage != null ) {
			g.drawImage(bufferImage, 0, 0, this);
		} else {
			//TODO this always gets called, update only seems to work with Canvas?!
			render( g );
		}
	}
	
	public void render( Graphics g ) {
		Rectangle bounds = g.getClipBounds();
		g.setColor( Color.black );
		g.fillRect( bounds.x, bounds.y, bounds.width, bounds.height );
		RotatedWorld world = this.rotatedWorld;
		if( this.viewport != null ) {
			int extraHeight = world.getWorldDepth()-world.getBlockDepth();
			
			int startBlockX = this.viewport.getBlockX();
			int startBlockY = this.viewport.getBlockY();
			int blockXOffset = -(this.viewport.getWorldX() % world.getBlockWidth());
			int blockYOffset = -(( this.viewport.getRearWorldY() % world.getBlockHeight() ) / TerrainConstants.Y_SPACING) - extraHeight;
			
			int visibleBlocksWidth = (this.getWidth() - blockXOffset) / world.getBlockWidth() + 1;
			int visibleBlocksHeight = ((this.getHeight()+ extraHeight)*2) / world.getBlockHeight() + 1;
			int visibleBlocksDepth = world.getBlocksZ();
			
			//g.setColor( Color.green );
			for( int x=visibleBlocksWidth; x>0; ) {
				x--;
				int px = x * world.getBlockWidth() + blockXOffset;
				int tx = startBlockX + x;
				for( int y=0; y<visibleBlocksHeight; y++ ) {
 					int py = (y * world.getBlockHeight()) / TerrainConstants.Y_SPACING  + blockYOffset;
					int ty = startBlockY + y;
					BlockSurfaceImages[] blockSurfaceImagesXY;
					blockSurfaceImagesXY = this.viewport.getBlockSurfaceImages(tx, ty);
					if( blockSurfaceImagesXY != null ) {
						for( int z=visibleBlocksDepth; z>this.viewport.getCurrentLayer(); ) {
							z--;
							int tz = z;
							int bz = tz;
							int pz = (z - this.viewport.getCurrentLayer()) * world.getBlockDepth();
							BlockSurfaceImages bsi = blockSurfaceImagesXY[ bz ];
							if( bsi != null ) {
								g.translate( px, py + pz );
								bsi.paint( g, z==this.viewport.getCurrentLayer() );
								if( showSurfaceCounts ) {
									int[][] surfaceCounts = bsi.getSurfaces();
									if( surfaceCounts != null ) {
										for( int sx=0; sx<surfaceCounts.length; sx++ ) {
											int[] surfaceCountsX = surfaceCounts[sx];
											for( int sy=0; sy<surfaceCountsX.length; sy++ ) {
												int count = surfaceCountsX[sy]*2;
												if( count > 0 ) {
													g.setColor( new Color( count | (count<<8) | (count<<16) ) );
													g.drawLine(sx, sy, sx, sy);
												}
											}
										}
									}
								}
								if( showDepths ) {
									int maxDepth = world.getBlockHeight();
									PixelDepth[][] pixelDepths = bsi.getPixelDepths();
									List<Node<SurfaceInsertion>> insertions = bsi.getSurfaceInsertions();
									Point p = new Point();
									for(int i=0; i<insertions.size(); i++ ) {
										SurfaceInsertion insertion = insertions.get( i ).getValue();
										Rectangle insertionBounds = insertion.getBounds();
										for( int sx=insertionBounds.x+insertionBounds.width; sx>insertionBounds.x; ) {
											sx--;
											p.x = sx;
											boolean previousWasNegative = true;
											for( int sy=insertionBounds.y+insertionBounds.height; sy>insertionBounds.y; ) {
												sy--;
												p.y = sy;
												int depthStart = insertion.getDepth(p);
												Color toUse;
												if( depthStart >= 0 ){
													if( previousWasNegative ) {
														toUse = Color.red;
														previousWasNegative = false;
													} else {
														Color c = Color.white;
														toUse = new Color( 
																(c.getRed() * depthStart) / maxDepth, 
																(c.getGreen() * depthStart) / maxDepth, 
																(c.getBlue() * depthStart) / maxDepth
														);													}
													
													try {
														g.setColor( toUse );
														g.drawLine(sx, sy, sx, sy);													
													} catch( Exception ex ) {
														ex.printStackTrace();
													}
												} else {
													previousWasNegative = true;
												}
												
											}
										}
									}
//									if( pixelDepths != null ) { 
//											
//										for( int sx=0; sx<pixelDepths.length; sx++ ) {
//											PixelDepth[] pixelDepthsX = pixelDepths[sx];
//											for( int sy=0; sy<pixelDepthsX.length; sy++ ) {
//												if( pixelDepthsX[sy] != null ) {
//													int count = pixelDepthsX[sy].getNumDepths();
//													Color c;
//													if( count > 0 ) {
//														switch( count ) {
//														case 2:
//															c = Color.red;
//															break;
//														case 4:
//															c = Color.green;
//															break;
//														case 6:
//															c = Color.blue;
//															break;
//														default:
//															c = Color.white;
//														}
//														int depthStart = pixelDepthsX[sy].getFirstDepthStart();
//														Color toUse = new Color( 
//																(c.getRed() * depthStart) / maxDepth, 
//																(c.getGreen() * depthStart) / maxDepth, 
//																(c.getBlue() * depthStart) / maxDepth
//														);
//														g.setColor( toUse );
//														g.drawLine(sx, sy, sx, sy);
//													}
//												}
//											}
//										}
//									}
								}
								g.translate( -px, -(py + pz) );
							}
						}
					}
					if( showPositions ){
						Color color;
						if( blockSurfaceImagesXY == null ){
							color = Color.red;
						}else if( blockSurfaceImagesXY[0] == null ) {
							color = Color.yellow;
						}else{
							color = Color.white;
						}
						g.setColor( color );
						Point p = viewport.getBlockPosition(tx, ty, new Point());
						String s = "("+p.x+","+p.y+")";
						g.drawString( 
								s,
								px+(world.getBlockWidth() - g.getFontMetrics().stringWidth(s))/2, 
								py+g.getFontMetrics().getHeight()
						);
						p = viewport.getViewPosition(p.x, p.y, new Point());
						s = "("+p.x+","+p.y+")";
						g.drawString( 
								s,
								px+(world.getBlockWidth() - g.getFontMetrics().stringWidth(s))/2, 
								py+g.getFontMetrics().getHeight()*2
						);
						
						g.drawLine(px, py, px+world.getBlockWidth(), py);
						g.drawLine(px, py, px, py+world.getBlockHeight()/2);
					}
				}
			}
		}
		
		if( blockGridTiles != null ) {
			
			int xoffset = -this.viewport.getWorldX();
			int extraHeight = world.getWorldDepth()-world.getBlockDepth();
			int yoffset = -(( this.viewport.getRearWorldY() ) / TerrainConstants.Y_SPACING) - extraHeight - this.viewport.getCurrentLayer() * world.getBlockDepth();
			
			g.setColor( Color.red );
			int gridXs = world.getXGridTilesPerBlock();
			int gridYs = world.getYGridTilesPerBlock();
			int pathXs = world.getXPathTilesPerBlock();
			int pathYs = world.getYPathTilesPerBlock();
			
			if( this.highlightedPath != null ) {
				for( PathElement pathElement : highlightedPath ) {
					int pathX = pathElement.getX();
					int pathY = pathElement.getY();
					int index = pathElement.getIndex();
					BlockGridTiles blockGridTiles = this.blockGridTiles[pathX/pathXs][pathY/pathYs];
					WalkablePath path = blockGridTiles.getPath( 
							pathX%pathXs, 
							pathY%pathYs, 
							index 
					);
					Point3D previousPoint = path.getPoints().get( 0 );
					for( int i=0; i<path.getPoints().size(); i++ ) {
						Point3D currentPoint = path.getPoints().get( i );
						g.drawLine(
								previousPoint.x + xoffset, 
								BlockSurfaceImageFactory.getPixelY(previousPoint.y, previousPoint.z) + yoffset, 
								currentPoint.x + xoffset, 
								BlockSurfaceImageFactory.getPixelY(currentPoint.y, currentPoint.z) + yoffset
						);
						previousPoint = currentPoint;
					}
				}
			}
			
			{
				int tx = TerrainUtils.getTx( this.highlightedGridX, this.highlightedGridY, world);
				int ty = TerrainUtils.getTy( this.highlightedGridX, this.highlightedGridY, world);
	
				BlockGridTiles blockGridTiles = this.blockGridTiles[this.highlightedGridX/gridXs][this.highlightedGridY/gridYs];
				
				GridTile[] gridTiles = blockGridTiles.getGridTiles( 
						this.highlightedGridX%gridXs, 
						this.highlightedGridY%gridYs
				);
				if( gridTiles.length > this.highlightedIndex && this.highlightedIndex >= 0 ) {
					GridTile gridTile = gridTiles[highlightedIndex];
					int px = tx;
					int py = BlockSurfaceImageFactory.getPixelY(ty, gridTile.getZ() );
					g.drawOval(px + xoffset - 3, py + yoffset - 3, 6, 6 );
					g.drawString( "("+this.highlightedGridX+","+this.highlightedGridY+")", px+xoffset + 4, py+yoffset );
				}
			}			
			if( showSurfaces ) {
				int worldDepth = world.getWorldDepth();
				int startX = this.viewport.getBlockX();
				int startY = this.viewport.getBlockY();
				int endX = Math.min(world.getBlocksX()-1, startX + this.viewport.getSurfaceImagesWidth() - 1);
				int endY = Math.min(world.getBlocksY()-1, startY + this.viewport.getSurfaceImagesHeight() - 1);
				int level = this.viewport.getCurrentLayer() * world.getBlockDepth();
				for(int x = Math.max(0,startX); x<=endX; x++) {
					for(int y = Math.max(0,startY); y<=endY; y++) {
						Point p = new Point();
						viewport.getBlockPosition(x, y, p);
						int[][][] surfaces = surfaceDAO.retrieveObject(
								p
						);
						TransformedBlock<int[]> rotatedSurfaces = new TransformedBlock<int[]>(
								surfaces, 
								world.getOriginalWorld().getBlockWidth(), 
								world.getOriginalWorld().getBlockHeight()
						);
						rotatedSurfaces.setRotation(world.getRotation(), world.getRotation().getRotationTransformation(world.getOriginalWorld().getBlockMidPointTimes2()));
						for(int sx=0; sx<rotatedSurfaces.getWidth(); sx++) {
							for(int sy=0; sy<rotatedSurfaces.getHeight(); sy++) {
								int[] surface = rotatedSurfaces.getPart(sx,sy);
								int index = Arrays.binarySearch(surface, level);
								if( index < 0 ) {
									index = -(index + 1);
								}
								if( index % 2 == 0 ) {
									int sz = surface[index];
									if( sx == 0 || sy == 0 ) {
										g.setColor( Color.GREEN );
									} else {
										switch( surface.length ) {
										case 2:
										default:
											Color c = new Color( ColorUtils.averageColors(0xFF0000, sz, 0x0000FF, worldDepth-sz) );
											g.setColor( c );
											break;
//										default:
//											g.setColor( Color.green );
//											break;
										}
	
									}
									int px = sx + x * world.getBlockWidth();
									int py = BlockSurfaceImageFactory.getPixelY(
											sy + y * world.getBlockHeight(), 
											sz
									);
									g.drawLine(px + xoffset, py + yoffset,px + xoffset, py + yoffset);
								}
							}
						}
					}
				}
			}
			if( showPaths ) {
				for( int x = 0; x<this.blockGridTiles.length; x++ ) {
					for( int y=0; y<this.blockGridTiles[x].length; y++ ) {
						for( int px = 0; px<pathXs; px++ ) {
							for( int py = 0; py<pathYs; py++ ) {
								WalkablePath[] paths = this.blockGridTiles[x][y].getPaths(px, py);
								for( int i=0; i<paths.length; i++ ) {
									WalkablePath path = paths[i];
									Color color;
									// TODO check to index properly
									if( (path.getFromGridIndex() < 0 || path.getFromGridIndex() >= 255 ) && 
										(path.getToGridIndex() < 0 || path.getToGridIndex() >= 255 ) 
									) {
										color = Color.orange;
									} else if( path.getFromGridIndex() < 0 ) {
										color = Color.red;
									} else if( path.getToGridIndex() < 0 ) {
										color = Color.yellow;
									} else if( path.getToGridIndex() >= 255 ) {
										color = Color.magenta;
									} else if( path.getFromGridIndex() >= 255 ) {
										color = Color.blue;
									} else {
										color = Color.cyan;
									}
									g.setColor(color);
									List<Point3D> points = path.getPoints();
									Point3D previousPoint = points.get( points.size() - 1 );
									for( int j=points.size()-1; j>0; ) {
										j--;
										Point3D current = points.get(j);
										if( current.z >= getViewZ() && previousPoint.z >= getViewZ() ) {
											int px1 = current.x + xoffset;
											int py1 = BlockSurfaceImageFactory.getPixelY(
													current.y, 
													current.z
											) + yoffset;
											int px2 = previousPoint.x + xoffset;
											int py2 = BlockSurfaceImageFactory.getPixelY(
													previousPoint.y, 
													previousPoint.z
											) + yoffset;
											g.drawLine( px1, py1, px2, py2 );
											previousPoint = current;
										}
									}
								}
							}
						}
					}
				}
			}
			if( showGridTiles ) {
				for( int x = 0; x<this.blockGridTiles.length; x++ ) {
					for( int y=0; y<this.blockGridTiles[x].length; y++ ) {
						for( int gx = 0; gx<gridXs; gx++ ) {
							for( int gy = 0; gy<gridYs; gy++ ) {
								GridTile[] tiles = this.blockGridTiles[x][y].getGridTiles(gx, gy);
								int wgx = gx + (x<<world.getxGridTilesPerBlockShift());
								int wgy = gy + (y<<world.getyGridTilesPerBlockShift());
								int tx = TerrainUtils.getTx(wgx, wgy, world);
								int ty = TerrainUtils.getTy(wgx, wgy, world);
								outer: for( int i=0; i<tiles.length; i++ ) {
									GridTile tile = tiles[i];
									int tz = tile.getZ();
									if( tz >= getViewZ() ) {
										int px = tx + xoffset;
										int py = BlockSurfaceImageFactory.getPixelY( ty, tz ) + yoffset;
										Color c;
										int[][] pathIndexes = tile.getPathIndexes();
										if( pathIndexes == null ) {
											c = Color.red;
										} else {
											c = Color.white;
											for( int l=0; l<pathIndexes.length; l++ ) {
												if( pathIndexes[l] == null ) {
													switch( l ) {
													case BlockGridTiles.INDEX_NE:
														c = Color.orange;
														break;
													case BlockGridTiles.INDEX_SE:
														c = Color.green;
														break;
													case BlockGridTiles.INDEX_SW:
														c = Color.cyan;
														break;
													default:
														c = Color.blue;
														break;
													}
													break;
												} else {
													for( int m=0; m<pathIndexes[l].length; m++ ) {
														if( pathIndexes[l][m] < 0 ) {
															c = Color.magenta;
															break outer;
														}
													}
												}
											}
										}
										g.setColor( c );
										g.drawOval(px-2, py-2, 4, 4);
									}
								}
							}
						}
					}
				}
			}
			if( renderAll ) {
				for( int x = 0; x<this.blockGridTiles.length; x++ ) {
					for( int y=0; y<this.blockGridTiles[x].length; y++ ) {
						for( int gx = 0; gx<pathXs; gx++ ) {
							for( int gy = 0; gy<pathYs; gy++ ) {
								if( 
										x * world.getXGridTilesPerBlock() + gx == this.highlightedGridX &&
										y * world.getYGridTilesPerBlock() + gy == this.highlightedGridY 
								){
									
									WalkablePath[] paths = this.blockGridTiles[x][y].getPaths(gx, gy);
									for( int i=0; i<paths.length; i++ ) {
										WalkablePath path = paths[i];
										List<Point3D> points = path.getPoints();
										if( points.size() > 0 ) {
											Point3D previous = points.get( 0 );
											for( int l=1; l<points.size(); l++ ) {
												Point3D current = points.get( l );
												int px = previous.x;
												int py = BlockSurfaceImageFactory.getPixelY(previous.y, previous.z);
												int cx = current.x;
												int cy = BlockSurfaceImageFactory.getPixelY(current.y, current.z);
												g.drawLine(px + xoffset, py + yoffset, cx + xoffset, cy + yoffset);
												previous = current;
											}
										}
									}
								}
							}
						}
						for( int gx = 0; gx<gridXs; gx++ ) {
							for( int gy = 0; gy<gridYs; gy++ ) {
								GridTile[] gridTiles = this.blockGridTiles[x][y].getGridTiles(gx, gy);
								for( int i=0; i<gridTiles.length; i++ ) {
									GridTile gridTile = gridTiles[ i ];
									int tx = TerrainUtils.getTx(
											x * world.getXGridTilesPerBlock() + gx, 
											y * world.getYGridTilesPerBlock() + gy, 
											world
									);
									int ty = TerrainUtils.getTy(
											x * world.getXGridTilesPerBlock() + gx, 
											y * world.getYGridTilesPerBlock() + gy, 
											world
									);
									int px = tx;
									int py = BlockSurfaceImageFactory.getPixelY(ty, gridTile.getZ());
									int[][] indexes = gridTile.getPathIndexes();
									for( int j=0; j<indexes.length; j++ ) {
										int apx;
										int apy;
										switch( j ) {
										case BlockGridTiles.INDEX_NE:
											apx = px + 5;
											apy = py - 5;
											break;
										case BlockGridTiles.INDEX_SE:
											apx = px + 5;
											apy = py + 5;
											break;
										case BlockGridTiles.INDEX_SW:
											apx = px - 5;
											apy = py + 5;
											break;
										case BlockGridTiles.INDEX_NW:
											apx = px - 5;
											apy = py - 5;
											break;
										default:
											apx = px;
											apy = py;
											break;
										}
										if( indexes[j] == null ) {
											g.drawString( "*", apx + xoffset, apy + yoffset );										
										} else if( indexes[j].length == 0 ) {
											g.drawString( "-", apx + xoffset, apy + yoffset );																				
										} else if( indexes[j] != null ) {
											for( int k=0; k<indexes[j].length; k++ ) {
												g.drawString( Integer.toString( indexes[j][k] ), apx + xoffset, apy + yoffset );
											}
										}
									}
								}
							}
						}
	//					if( costs != null ) {
	//						for( int gx = 0; gx<gridXs; gx++ ) {
	//							for( int gy = 0; gy<gridYs; gy++ ) {
	//								int[] costs = this.costs[x * gridXs + gx][y * gridYs + gy];
	//								if( costs != null ) {
	//									for( int i=0; i<costs.length; i++ ) {
	//										int tx = TerrainUtils.getTx(x * gridXs + gx, y * gridYs + gy, world);
	//										int ty = TerrainUtils.getTy(x * gridXs + gx, y * gridYs + gy, world);
	//										g.drawString(
	//												Integer.toString( costs[i] ),
	//												tx + xoffset, 
	//												BlockSurfaceImageFactory.getPixelY(ty, 0) + yoffset 
	//										);
	//									}
	//								}
	//							}
	//						}
	//					}
					}
				}
			}
		}
		if( this.showEvents ) {
			InMemoryEventQueue eventQueue = (InMemoryEventQueue) this.getCurrentWorld().getEventQueue();
			List<Event> events = eventQueue.getEvents();
			FontMetrics fm = g.getFontMetrics();
			int y = fm.getHeight() * 2;
			g.setColor( Color.magenta );
			for( int i=0; i<events.size(); i++ ) {
				g.drawString( events.get( i ).toString(), 0, y + fm.getAscent() );
				y += fm.getHeight();
			}
		}
		// render the rotation
		g.setColor(Color.white);
		g.drawString(Integer.toString(this.getCurrentRotation().getDegrees()), 0, g.getFontMetrics().getAscent());
	}

	public BlockSurfaceImages retrieveObject(Point3DAndRotation p) {
		BlockSurfaceImages result;
		BlockSurfaceImages[] images;
		Rotation rotation = this.viewport.getCurrentRotation();
		if( p.rotation == rotation ) {
			Point mid = rotatedWorld.getWorldBlockMidPointTimes2();
			Point r = rotation.getOpposite().rotateInBox(p.x, p.y, mid.x, mid.y, new Point());
			images = this.viewport.getBlockSurfaceImages(r.x, r.y);
			if( images != null ) {
				result = images[p.z];
//				BlockSurfaceImages found = this.surfaceImagesDAO.retrieveObject(
//						p
//				);
//				if( found != result ) {
//					log.warning("fuck, fuck, fuck");
//					result = found;
//				}
			} else {
				// we don't
				result = this.surfaceImagesDAO.retrieveObject(
						p
				);
			}
		} else {
			result = this.surfaceImagesDAO.retrieveObject(p);
		}
		return result;
	}

	public void storeObject(
			Point3DAndRotation p,
			BlockSurfaceImages bsi
	) {
		this.surfaceImagesDAO.storeObject(
				p, bsi
		);
		BlockSurfaceImages[] images;
		Rotation rotation = this.viewport.getCurrentRotation();
		if( rotation == p.rotation ) {
			Point mid = rotatedWorld.getWorldBlockMidPointTimes2();
			Point r = rotation.getOpposite().rotateInBox(p.x, p.y, mid.x, mid.y, new Point());
			images = this.viewport.getBlockSurfaceImages(r.x, r.y);
			if( images != null ) {
				// this is a good place to ensure that the new BSIs have the correct context
				List<Node<SurfaceInsertion>> insertions = bsi.getSurfaceInsertions();
				for( int i=insertions.size(); i>0; ) {
					i--;
					Node<SurfaceInsertion> insertion = insertions.get( i );
					RendererChain rendererChain = insertion.getValue().getRendererChain();
					if( !rendererChain.isAttachedTo( this ) ) {
						rendererChain.attach(this);
					}
				}
				images[p.z] = bsi;
				// TODO repaint area
				repaint();
			}
		}
	}
	
	public boolean containsObject(Point3DAndRotation p) {
		BlockSurfaceImages[] images;
		if( p.rotation == this.viewport.getCurrentRotation() ) {
			images = this.viewport.getBlockSurfaceImages(p.x, p.y);
			if( images != null ) {
				return true;
			} else {
				return this.surfaceImagesDAO.containsObject( p );
			}
		} else {
			return this.surfaceImagesDAO.containsObject( p );
		}
	}

	public boolean removeObject(Point3DAndRotation key) {
		return this.surfaceImagesDAO.removeObject(key);
	}
	
	public List<SurfaceInsertion> getInsertionsAt( int sx, int sy ) {
		ArrayList<SurfaceInsertion> result;
		if( this.viewport != null ) {
			result = new ArrayList<SurfaceInsertion>();
			World world = getCurrentWorld();
			int tileWidth = world.getBlockWidth();
			int tileHeight = BlockSurfaceImageFactory.getPixelY(
					world.getBlockHeight(), 
					world.getBlockDepth()
			);
			int extraHeight = world.getWorldDepth()-world.getBlockDepth();		
			// search every block in order
			for( int x=this.viewport.getSurfaceImagesWidth(); x>0; ) { 
				x--;
				int bx = x + this.viewport.getBlockX();
				for( int y=this.viewport.getSurfaceImagesHeight(); y>0; ) {
					y--;
					int by = y + this.viewport.getBlockY();
					BlockSurfaceImages[] blockSurfaceImagesXY;
					blockSurfaceImagesXY = this.viewport.getBlockSurfaceImages(bx, by);
					if( blockSurfaceImagesXY != null ) {
						for( int z=this.viewport.getCurrentLayer(); z<world.getBlocksZ(); z++ ) {
							BlockSurfaceImages bsi;
							bsi = blockSurfaceImagesXY[z];
							if( bsi != null ) {
								int sxoff = x * tileWidth - this.viewport.getWorldX()%world.getBlockWidth();
								int syoff = BlockSurfaceImageFactory.getPixelY( 
										y * world.getBlockHeight() - this.viewport.getRearWorldY() % world.getBlockHeight() - extraHeight*2,
										(z-this.viewport.getCurrentLayer()) * world.getBlockDepth()
								);
								if( sx >= sxoff && sy >= syoff && 
									sx < sxoff + tileWidth &&
									sy < syoff + tileHeight ) {
									// it might contain it
									List<Node<SurfaceInsertion>> insertions = bsi.getSurfaceInsertions();
									// ordering??
									for( int i=insertions.size(); i>0; ) {
										i--;
										Node<SurfaceInsertion> node = insertions.get(i);
										SurfaceInsertion insertion = node.getValue();
										int lx = sx - sxoff;
										int ly = sy - syoff;
										if( insertion.contains(lx, ly) ) {
											result.add( insertion );
										}
									}
								}
							}
						}
					}
				}
			}
		} else {
			result = null;
		}
		return result;
	}

	public Point3D getTerrainPosition( int sx, int sy, Point3D p ) {
		RotatedWorld world = rotatedWorld;
		if( this.viewport != null ) {
			int extraHeight = world.getWorldDepth()-world.getBlockDepth();
			
			int startBlockX = this.viewport.getBlockX();
			int startBlockY = this.viewport.getBlockY();
			int blockXOffset = -(this.viewport.getWorldX() % world.getBlockWidth());
			int blockYOffset = -(( this.viewport.getRearWorldY() % world.getBlockHeight() ) / TerrainConstants.Y_SPACING) - extraHeight;

			int tileWidth = world.getBlockWidth();
			int tileHeight = world.getBlockHeight()/2 + world.getBlockDepth();
			
			int visibleBlocksWidth = (this.getWidth() - blockXOffset) / world.getBlockWidth() + 1;
			int visibleBlocksHeight = ((this.getHeight()+ extraHeight)*2) / world.getBlockHeight() + 1;
			int visibleBlocksDepth = world.getBlocksZ();
			
			//g.setColor( Color.green );
			for( int x=visibleBlocksWidth; x>0; ) {
				x--;
				int px = x * world.getBlockWidth() + blockXOffset;
				int tx = startBlockX + x;
				for( int y=visibleBlocksHeight; y>0;  ) {
					y--;
 					int py = (y * world.getBlockHeight()) / TerrainConstants.Y_SPACING  + blockYOffset;
					int ty = startBlockY + y;
					BlockSurfaceImages[] blockSurfaceImagesXY;
					blockSurfaceImagesXY = this.viewport.getBlockSurfaceImages(tx, ty);
					if( blockSurfaceImagesXY != null ) {
						for( int z=this.viewport.getCurrentLayer(); z<world.getBlocksZ(); z++) {
							int tz = z;
							int pz = (z - this.viewport.getCurrentLayer()) * world.getBlockDepth();
							BlockSurfaceImages bsi = blockSurfaceImagesXY[ tz ];
							if( bsi != null ) {
								if( px <= sx && (px + tileWidth) > sx && py+pz <= sy && (py+pz+tileHeight) > sy ) {
									// this is us
									PixelDepth[][] depths = bsi.getPixelDepths();
									if( depths != null ) { 
										
										int localsy = sy-(py+pz);
										int localsx = sx-px;
										PixelDepth depth = depths[localsx][localsy];
										if( depth != null )
										{
											Point3D r = new Point3D();
											viewport.getBlockPosition(tx, ty, r);
											// TODO need to subtract localsy from max localsy
											int maxDepth = world.getBlockHeight();
											int zoffset = world.getBlockHeight()>>1;
							
											int firstDepth = depth.getFirstDepthStart();
							
											int lx = localsx;
											int ly = maxDepth - ((firstDepth+1)<<1);
//											int ly = (maxDepth - firstDepth*2);
											
											Point l = new Point();
											Point blockMidPoint = world.getBlockMidPointTimes2();
											getCurrentRotation().rotateInBox(
													lx, ly, 
													blockMidPoint.x, 
													blockMidPoint.y, 
													l
											);
							
											r.x = r.x * world.getBlockWidth() + l.x;
											r.y = r.y * world.getBlockHeight() + l.y;
											r.z = tz * world.getBlockDepth() + localsy + firstDepth - zoffset;
											return r;
										}
									}
								}
							}
						}
					}
				}
			}
		}
		return null;
	}

	public void requestRepaint(EntityRenderer renderer) {
		repaint();
	}
	
	public RendererChain knowEntity( Entity entity ) {
		RendererChain result = this.knownEntities.get( entity );
		if( result == null ) {
			result = this.rendererFactory.getRendererChain(getCurrentWorld(), entity, this.getCurrentRotation());
			if( !result.isAttachedTo( this ) ) {
				result.attach(this);
			}
			this.knownEntities.put( entity, result );
			entity.addEntityListener(EntityEventType.position, this);
		}
		return result;
	}
	
	public void considerEntity(Entity entity) {
		if( entity instanceof Physical ) {
			boolean alreadyWatching = knownEntities.containsKey(entity);
			if( !alreadyWatching ) {
				Physical physical = (Physical)entity;
				Dimensions3D dimensions = physical.getDimensions();
				Point3D position = physical.getPosition();
				Rectangle bounds = new Rectangle(
						position.x - dimensions.width/2,
						position.y - dimensions.height/2,
						dimensions.width, 
						dimensions.height
				);
				Rectangle viewBounds = viewport.getBounds();
				Rectangle intersection = bounds.intersection(viewBounds);
				if( intersection.width > 0 && intersection.height > 0 ) {
					// watch the entity
					RendererChain renderer = knowEntity(entity);
					RotatedWorld world = new RotatedWorld( getCurrentWorld(), getCurrentRotation() );
					addRenderer( world, renderer, position, dimensions );
				}
			}			
		}
	}
	
	public void forgetAllEntities() {
		ArrayList<Entity> entities = new ArrayList<Entity>( this.knownEntities.keySet() );
		for( int i=entities.size(); i>0; ) {
			i--;
			Entity entity = entities.get( i );
			forgetEntity(entity);
		}		
	}

	public void forgetEntity(Entity entity) {
		RendererChain renderer = this.knownEntities.remove( entity );
		if( renderer != null ) {
			renderer.detach(this);
			entity.removeEntityListener(EntityEventType.position, this);
			Physical physical = (Physical)entity;
			RotatedWorld world = new RotatedWorld( getCurrentWorld(), getCurrentRotation() );
			removeRenderer( world, renderer, physical.getPosition(), physical.getDimensions() );
		}
	}


	public boolean addRenderer( RotatedWorld world, RendererChain renderer, Point3D position, Dimensions3D dimensions ) {
		if( !SwingUtilities.isEventDispatchThread() ) {
			throw new RuntimeException("must be swing thread");
		}
		// work out the blocks effected, add a renderer
		boolean result = false;
		Dimension dimension = renderer.getHead().getDimension();
		Point grabPoint = renderer.getHead().getGrabPoint();
		Rotation rotation = world.getRotation();
		
		int startX;
		int startY;
		int startZ;

		
		//TODO rotate the grab point
		startX = position.x - grabPoint.x;
		startY = position.y;
		startZ = position.z - grabPoint.y;			
		
		Rectangle3D bounds = new Rectangle3D(
				startX, 
				startY, 
				startZ, 
				startX + dimension.width, 
				startY, 
				startZ + dimension.height
		); 
		bounds.rotate(rotation, new Point( position.x*2, position.y*2 ));

		// get the blocks that it overlaps
		int startBlockX = bounds.minX >> world.getBlockWidthShift();
		int endBlockX = bounds.maxX >> world.getBlockWidthShift();
		int startBlockY = bounds.minY >> world.getBlockHeightShift();
		int endBlockY = bounds.maxY >> world.getBlockHeightShift();
		int startBlockZ = Math.max( 0, bounds.minZ / world.getBlockDepth() );
		int endBlockZ = Math.min(world.getBlocksZ()-1, bounds.maxZ / world.getBlockDepth());
		
//		log.info( "adding ("+startBlockX+","+startBlockY+","+startBlockZ+") -> ("+endBlockX+","+endBlockY+","+endBlockZ+")" );

		Point blockMidPoint = world.getWorldBlockMidPointTimes2();
		Point rotatedPoint = new Point();
//		rotatedPoint.y = blockY;
		
		for( int bx=startBlockX; bx<=endBlockX; bx++ ) {
			for( int by=startBlockY; by<=endBlockY; by++ ) {
				// rotate
				rotation.getOpposite().rotateInBox(bx, by, blockMidPoint.x, blockMidPoint.y, rotatedPoint);
				//viewport.getBlockPosition(bx, by, rotatedPoint);
//				rotatedPoint.x = bx;
				
				BlockSurfaceImages[] bsis = viewport.getBlockSurfaceImages(
						rotatedPoint.x, 
						rotatedPoint.y
				);
				
				if( bsis != null ) {
					result = true;
					for( int bz=startBlockZ; bz<=endBlockZ; bz++ ) {
						BlockSurfaceImages bsi = bsis[bz];
						if( bsi != null ) {
							Point3D key = new Point3D( 
									bx, 
									by, 
									bz 
							);
//							SurfaceRendererInsertion insertion = TerrainUtils.createInsertion( 
//									world, 
//									key, 
//									imageBounds, 
//									depth, 
//									y, 
//									renderer 
//							);
							SurfaceRendererInsertion insertion = TerrainUtils.createInsertion(
									world, 
									key, 
									renderer
							);
							if( insertion != null ) {
								bsi.addInsertion( insertion );
							}
						}
					}
				}				
			}
		}
		return result;
	}

	public void removeRenderer( RotatedWorld world, RendererChain renderer, Point3D position, Dimensions3D dimensions ) {
		if( !SwingUtilities.isEventDispatchThread() ) {
			throw new RuntimeException("must be swing thread");
		}
		// work out the blocks effected, add a renderer
		Rotation rotation = world.getRotation();
		
		Dimension dimension = renderer.getHead().getDimension();
		Point grabPoint = renderer.getHead().getGrabPoint();
		int startX = position.x - grabPoint.x;
		int startY = position.y;
		int startZ = position.z - grabPoint.y;
		
		Rectangle3D bounds = new Rectangle3D(
				startX, 
				startY, 
				startZ, 
				startX + dimension.width, 
				startY, 
				startZ + dimension.height
		); 
		bounds.rotate(rotation, new Point( position.x*2, position.y*2 ));

		// get the blocks that it overlaps
		int startBlockX = bounds.minX >> world.getBlockWidthShift();
		int endBlockX = bounds.maxX >> world.getBlockWidthShift();
		int startBlockY = bounds.minY >> world.getBlockHeightShift();
		int endBlockY = bounds.maxY >> world.getBlockHeightShift();
		int startBlockZ = Math.max( 0, bounds.minZ / world.getBlockDepth() );
		int endBlockZ = Math.min( world.getBlocksZ()-1, bounds.maxZ / world.getBlockDepth() );
		
//		log.info( "removing ("+startBlockX+","+startBlockY+","+startBlockZ+") -> ("+endBlockX+","+endBlockY+","+endBlockZ+")" );

		
		Point blockMidPoint = world.getWorldBlockMidPointTimes2();
		Point rotatedPoint = new Point();

		for( int bx=startBlockX; bx<=endBlockX; bx++ ) {
			for( int by=startBlockY; by<=endBlockY; by++ ) {
				rotation.getOpposite().rotateInBox(bx, by, blockMidPoint.x, blockMidPoint.y, rotatedPoint);
//
//				viewport.getBlockPosition(bx, by, rotatedPoint);
				BlockSurfaceImages[] bsis = viewport.getBlockSurfaceImages(
						rotatedPoint.x, 
						rotatedPoint.y
				);
				
				if( bsis != null ) {
					for( int bz=startBlockZ; bz<=endBlockZ; bz++ ) {
						BlockSurfaceImages bsi = bsis[bz];
						if( bsi != null ) {
							List<Node<SurfaceInsertion>> insertions = bsi.getSurfaceInsertions();
							Node<SurfaceInsertion> removedNode = null;
							boolean found = false;
							for( int i=insertions.size(); i>0; ) {
								i--;
								Node<SurfaceInsertion> insertion = insertions.get(i);
								if( insertion.getValue().getRendererChain() == renderer ) {
									insertions.remove(i);
									removedNode = insertion;
									found = true;
									break;
								}
							}
							if( !found ) {
								log.info("couldn't find entity to be removed ("+bx+","+by+") "+position+"/"+dimensions);
							} else {
								for( int i=insertions.size(); i>0; ) {
									i--;
									Node<SurfaceInsertion> insertion = insertions.get(i);
									insertion.removeConnection( removedNode );
								}
							}
						}
					}
				}
			}
		}		
	}
	
	public void addInteractionHandler( InteractionHandler interactionHandler ) {
		int pos = Collections.binarySearch(this.eventAdapter.interactionHandlers, interactionHandler, this.eventAdapter);
		if( pos < 0 ) {
			pos = -(pos+1); 
		}
		this.eventAdapter.interactionHandlers.add( pos, interactionHandler );
	}
	
	public void removeInteractionHandler( InteractionHandler interactionHandler ) {
		this.eventAdapter.interactionHandlers.remove( interactionHandler );
	}
	
	@SuppressWarnings("unchecked")
	public void entityChanged(final Entity source, final EntityEvent event) {
		// assume that we're talking about an entity movement
		// do this in the AWT thread
		Runnable r = new Runnable() {
			public void run() {
				EntityChangeEvent<Point3D> changeEvent = (EntityChangeEvent<Point3D>)event;
				RendererChain renderer = knownEntities.get( source );
				if( renderer != null ) {
					Dimensions3D dimensions = ((Physical)source).getDimensions();
					RotatedWorld world = new RotatedWorld( getCurrentWorld(), getCurrentRotation() );
					removeRenderer(world, renderer, changeEvent.getFromAttribute(), dimensions);
					if( !addRenderer(world, renderer, changeEvent.getToAttribute(), dimensions) ) {
						// stop listening, it's dead to us
						renderer.detach(IsometricTerrainView.this);
						source.removeEntityListener(EntityEventType.position, IsometricTerrainView.this);
					}
					// TODO repaint changed area
					repaint();
				} else {
					log.warning("no renderer for entity being listened upon, cleaning up");
					source.removeEntityListener(EntityEventType.position, IsometricTerrainView.this);
				}				
			}
		};
		SwingUtilities.invokeLater(r);
	}



	private class IsometricTerrainViewEventAdapter 
		implements MouseListener, MouseMotionListener, MouseWheelListener, KeyListener, Comparator<InteractionHandler> {
		
		private List<InteractionHandler> interactionHandlers;
		
		public IsometricTerrainViewEventAdapter() {
			this.interactionHandlers = new ArrayList<InteractionHandler>();
		}

		public void mouseWheelMoved(MouseWheelEvent e) {
			fireMouseWheelEvent( e );
		}

		public void keyPressed(KeyEvent e) {
			fireKeyEvent( e );
		}

		public void keyReleased(KeyEvent e) {
			fireKeyEvent( e );
		}

		public void keyTyped(KeyEvent e) {
			fireKeyEvent( e );
		}

		public void mouseDragged(MouseEvent e) {
			fireMouseEvent( e );
		}

		public void mouseMoved(MouseEvent e) {
			fireMouseEvent( e );
		}
		
		public void mouseClicked(MouseEvent e) {
			fireMouseEvent( e );
		}

		public void mousePressed(MouseEvent e) {
			fireMouseEvent( e );
		}

		public void mouseReleased(MouseEvent e) {
			fireMouseEvent( e );
		}
		
		public void mouseEntered(MouseEvent e) {
			fireMouseEvent( e );
		}

		public void mouseExited(MouseEvent e) {
			fireMouseEvent( e );
		}

		public void fireMouseEvent( MouseEvent e ) {
			ArrayList<MouseEvent> events = new ArrayList<MouseEvent>(1);
			events.add(e);
			for( int i=this.interactionHandlers.size(); i>0; ) {
				i--;
				ArrayList<MouseEvent> nextEvents = new ArrayList<MouseEvent>(events.size());
				InteractionHandler h = this.interactionHandlers.get( i );
				for( int j=0; j<events.size(); j++) {
					MouseEvent me = events.get(j);
					MouseEvent[] children = h.handleMouseEvent(me);
					if( children != null ) {
						Collections.addAll(nextEvents, children);
					}
				}
				events = nextEvents;
				if( events.size() == 0 ) {
					break;
				}
			}
		}
		
		public void fireKeyEvent( KeyEvent e ) {
			ArrayList<KeyEvent> events = new ArrayList<KeyEvent>(1);
			events.add(e);
			for( int i=this.interactionHandlers.size(); i>0; ) {
				i--;
				ArrayList<KeyEvent> nextEvents = new ArrayList<KeyEvent>(events.size());
				InteractionHandler h = this.interactionHandlers.get( i );
				for( int j=0; j<events.size(); j++) {
					KeyEvent me = events.get(j);
					KeyEvent[] children = h.handleKeyEvent(me);
					if( children != null ) {
						Collections.addAll(nextEvents, children);
					}
				}
			}
		}
		
		public void fireMouseWheelEvent( MouseWheelEvent e ) {
			ArrayList<MouseWheelEvent> events = new ArrayList<MouseWheelEvent>(1);
			events.add(e);
			for( int i=this.interactionHandlers.size(); i>0; ) {
				i--;
				ArrayList<MouseWheelEvent> nextEvents = new ArrayList<MouseWheelEvent>(events.size());
				InteractionHandler h = this.interactionHandlers.get( i );
				for( int j=0; j<events.size(); j++) {
					MouseWheelEvent me = events.get(j);
					MouseWheelEvent[] children = h.handleMouseWheelEvent(me);
					if( children != null ) {
						Collections.addAll(nextEvents, children);
					}
				}
			}
		}

		public int compare(InteractionHandler o1, InteractionHandler o2) {
			return o1.getPriority() - o2.getPriority();
		}
	}
	
}